Skip to content

Conversation

@ban-xiu
Copy link
Contributor

@ban-xiu ban-xiu commented Jan 3, 2026

What this PR does / why we need it?

Fix: 修复自定义 List 子类反序列化时元素类型解析为 JSONObject 的问题 issus_3926

恳请帮忙审查并合并此 PR,希望能为项目贡献一份力量

根本原因

在 ObjectReaderCreator.createFieldReader() 方法中,当字段类型是自定义 List 子类(如 MyList.class )时:

  1. fieldTypeResolved 不是 ParameterizedType ,无法提取泛型类型参数
  2. fieldType 是普通 Class (不是 ParameterizedType ),也无法提取泛型类型参数
  3. 最终 itemType 被设置为 Object.class ,导致反序列化时创建 JSONObject

解决方案

在 ObjectReaderCreator.java 的 List 类型解析逻辑中,新增第三步类型解析策略:

  1. 第一步 :从 fieldTypeResolved 提取类型参数(原有逻辑)
  2. 第二步 :从 fieldType 提取类型参数(原有逻辑)
  3. 第三步 :遍历 fieldType 的泛型超类层次结构,查找包含泛型类型参数的 Collection 超类
    另外处理了fieldTypeResolved fieldClassResolved为空的情况

Summary of your change

core/src/main/java/com/alibaba/fastjson2/reader/ObjectReaderCreator.java,在 createFieldReader() 方法中,针对 List 类型的字段解析逻辑进行了增强

core/src/test/java/com/alibaba/fastjson2_demo/CustomListTest.java,添加了测试用例验证修复效果

Please indicate you've done the following:

  • Made sure tests are passing and test coverage is added if needed.
  • Made sure commit message follow the rule of Conventional Commits specification.
  • Considered the docs impact and opened a new docs issue or PR with docs changes if needed.

@CLAassistant
Copy link

CLAassistant commented Jan 3, 2026

CLA assistant check
All committers have signed the CLA.

@jujn
Copy link
Collaborator

jujn commented Jan 3, 2026

谢谢你的支持,我最近也在研究这个,有几个建议供你参考:
①对于直接实现接口的场景(如下),目前的修复可能还存在一些缺陷

public class MyList implements List<String> { ... }

②单测可以补充一下 FieldBased 的场景:

MyDTO myDTO = JSON.parseObject("{\"myList\":[{\"name\":\"张三\"}]}", MyDTO.class, JSONReader.Feature.FieldBased);
MyObject myObject = myDTO.getMyList().get(0);

③单测类的命名、放置位置、编写风格 可以参考下其它已合并的 PR

@ban-xiu
Copy link
Contributor Author

ban-xiu commented Jan 3, 2026

@jujn 好的我研究一下

@ban-xiu
Copy link
Contributor Author

ban-xiu commented Jan 4, 2026

@jujn hi,已经完成了接口解析的补充和自测,请问可以帮忙review一下吗

@jujn
Copy link
Collaborator

jujn commented Jan 4, 2026

MyDTO myDTO = JSON.parseObject("{"myList":[{"name":"张三"}]}", MyDTO.class, JSONReader.Feature.FieldBased);
FieldBased 的场景也需要修复(在 ObjectReaderCreator 某一个 createFieldReader 中,你可以自己找一下hh),增加与 createFieldReaderMethod 相类似的逻辑即可。因为这两种情况,一个是基于setter解析,一个基于field。

@ban-xiu
Copy link
Contributor Author

ban-xiu commented Jan 5, 2026

@jujn 搞定了辛苦帮忙看看

@jujn
Copy link
Collaborator

jujn commented Jan 5, 2026

可以改成这样吗?

if (fieldTypeResolved instanceof ParameterizedType) {
  // 这里的逻辑不要动
  …… 
} else {
   调用 BeanUtils.resolveCollectionItemType
   ……
}

@ban-xiu
Copy link
Contributor Author

ban-xiu commented Jan 5, 2026

已修改

@jujn
Copy link
Collaborator

jujn commented Jan 5, 2026

resolveCollectionItemType 里可以复用 BeanUtils.getGenericSupertype 方法吗?(比如类似下面这样的逻辑)

    public static Type resolveCollectionItemType(Type fieldTypeResolved, Class<?> fieldClass) {
        Type contextType = fieldTypeResolved != null ? fieldTypeResolved : fieldClass;

        Type listType = getGenericSupertype(contextType, fieldClass, List.class);

        if (listType == null || listType == List.class) {
            listType = getGenericSupertype(contextType, fieldClass, Collection.class);
        }

        listType = BeanUtils.resolve(contextType, fieldClass, listType);

        if (listType instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) listType;
            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();

            if (actualTypeArguments.length == 1) {
                return actualTypeArguments[0];
            }
        }
        return null;
    }

@ban-xiu
Copy link
Contributor Author

ban-xiu commented Jan 5, 2026

感谢指正,确实有很多可以复用的方法

@ban-xiu
Copy link
Contributor Author

ban-xiu commented Jan 5, 2026

已经做出修改

@ban-xiu
Copy link
Contributor Author

ban-xiu commented Jan 5, 2026

不知道为什么,图片中的评论我展示不出来,现在调整过了

@jujn
Copy link
Collaborator

jujn commented Jan 6, 2026

等温老师有时间来审核吧,这里我感觉很容易留下逻辑漏洞

@wenshao
Copy link
Member

wenshao commented Jan 10, 2026

很好的改进,这个问题在Map上也有,欢迎继续提PR

@wenshao wenshao merged commit 6bbb2fd into alibaba:main Jan 10, 2026
21 checks passed
wenshao pushed a commit that referenced this pull request Jan 10, 2026
* fix_issus_3926

* support generic type resolution for custom collection subclasses and interfaces

* move test

* fix field-based mode

* use resolveCollectionItemType

* fix resolveCollectionItemType

* fix fieldReaderList

* fix fieldReaderList return
@ban-xiu
Copy link
Contributor Author

ban-xiu commented Jan 11, 2026

我尝试了以下这个例子,可以正常运行,似乎Map已经支持了

package com.alibaba.fastjson2.issues_3900;

import com.alibaba.fastjson2.JSON;
import org.junit.jupiter.api.Test;

import java.util.HashMap;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Issue3926_Map {
    /**
     * 测试Map的key和value类型被继承的情况
     */
    public static class MyDTO1 {
        private MyMap1 myMap1;

        public MyMap1 getMyMap1() {
            return myMap1;
        }

        public void setMyMap1(MyMap1 myMap1) {
            this.myMap1 = myMap1;
        }
    }

    /**
     * 自定义Map子类,使用继承的key和value类型
     */
    public static class MyMap1 extends HashMap<MyKey, MyValue> {
    }

    /**
     * 基础Key类
     */
    public static class BaseKey {
        protected int id;

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }
    }

    /**
     * 自定义Key类,继承自BaseKey
     */
    public static class MyKey extends BaseKey {
        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            MyKey myKey = (MyKey) o;
            return id == myKey.id && (name != null ? name.equals(myKey.name) : myKey.name == null);
        }

        @Override
        public int hashCode() {
            return 31 * id + (name != null ? name.hashCode() : 0);
        }
    }

    /**
     * 基础Value类
     */
    public static class BaseValue {
        protected String job;

        public String getJob() {
            return job;
        }

        public void setJob(String job) {
            this.job = job;
        }
    }

    /**
     * 自定义Value类,继承自BaseValue
     */
    public static class MyValue extends BaseValue {
        private int age;

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }
    }

    /**
     * 测试Map的key和value类型被继承的情况
     */
    @Test
    public void testCustomMapWithInheritedTypes() {
        String json = "{\"myMap1\":{{\"name\":\"张三\"}:{\"job\":\"司机\",\"age\":30}}}";
        MyDTO1 myDTO1 = JSON.parseObject(json, MyDTO1.class);

        assertEquals(1, myDTO1.getMyMap1().size());

        for (java.util.Map.Entry<MyKey, MyValue> entry : myDTO1.getMyMap1().entrySet()) {
            MyKey key = entry.getKey();
            MyValue value = entry.getValue();

            // 验证值的属性
            assertEquals("张三", key.getName());
            assertEquals("司机", value.getJob());
            assertEquals(30, value.getAge());
        }
    }

    /**
     * 测试Map的key和value类型实现接口的情况
     */
    public static class MyDTO2 {
        private MyMap2 myMap2;

        public MyMap2 getMyMap2() {
            return myMap2;
        }

        public void setMyMap2(MyMap2 myMap2) {
            this.myMap2 = myMap2;
        }
    }

    /**
     * 自定义Map子类,使用实现接口的key和value类型
     */
    public static class MyMap2 extends HashMap<KeyImpl, ValueImpl> {
    }

    /**
     * Key接口
     */
    public interface KeyInterface {
        String getKey();
    }

    /**
     * 实现KeyInterface的类
     */
    public static class KeyImpl implements KeyInterface {
        private String key;

        @Override
        public String getKey() {
            return key;
        }

        public void setKey(String key) {
            this.key = key;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            KeyImpl keyImpl = (KeyImpl) o;
            return key != null ? key.equals(keyImpl.key) : keyImpl.key == null;
        }

        @Override
        public int hashCode() {
            return key != null ? key.hashCode() : 0;
        }
    }

    /**
     * Value接口
     */
    public interface ValueInterface {
        String getValue();
    }

    /**
     * 实现ValueInterface的类
     */
    public static class ValueImpl implements ValueInterface {
        private String value;

        @Override
        public String getValue() {
            return value;
        }

        public void setValue(String value) {
            this.value = value;
        }
    }

    /**
     * 测试Map的key和value类型实现接口的情况
     */
    @Test
    public void testCustomMapWithInterfaceTypes() {
        String json = "{\"myMap2\":{{\"key\":\"key1\"}:{\"value\":\"value1\"}}}";
        MyDTO2 myDTO2 = JSON.parseObject(json, MyDTO2.class);

        assertEquals(1, myDTO2.getMyMap2().size());

        for (java.util.Map.Entry<KeyImpl, ValueImpl> entry : myDTO2.getMyMap2().entrySet()) {
            KeyImpl key = entry.getKey();
            ValueImpl value = entry.getValue();

            // 验证值的属性
            assertEquals("key1", key.getKey());
            assertEquals("value1", value.getValue());
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants